package com.szx.usercenter.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.MD5;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.szx.usercenter.contance.FileProperties;
import com.szx.usercenter.domain.FilePan;
import com.szx.usercenter.domain.UserFileNumber;
import com.szx.usercenter.handle.CenterExceptionHandler;
import com.szx.usercenter.mapper.FilePanMapper;
import com.szx.usercenter.service.FilePanService;
import com.szx.usercenter.service.UserFileNumberService;
import com.szx.usercenter.util.*;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 25019
 * @description 针对表【file_pan】的数据库操作Service实现
 * @createDate 2024-06-02 18:55:24
 */
@Service
@Log4j2
public class FilePanServiceImpl extends ServiceImpl<FilePanMapper, FilePan> implements FilePanService {
    private final ExecutorService executorService = Executors.newFixedThreadPool(10); // 线程池大小可根据实际情况调整
    @Autowired
    FileProperties fileProperties;
    @Autowired
    UserFileNumberService fileNumberService;
    /**
     * 根据父ID获取所有父文件的fileName
     */
    List<String> fileNameList = new ArrayList<>();

    /**
     * 读取文件
     *
     * @param response
     * @param filePath
     */
    public static void readFile(HttpServletResponse response, String filePath) {
        OutputStream out = null;
        FileInputStream in = null;
        try {
            File file = new File(filePath);
            if (!file.exists()) {
                return;
            }
            in = new FileInputStream(file);
            byte[] byteData = new byte[1024];
            out = response.getOutputStream();
            int len = 0;
            while ((len = in.read(byteData)) != -1) {
                out.write(byteData, 0, len);
            }
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public FileUploadEnum uploadChunkFile(MultipartFile chunkFile, String fileName, Long fileSize, String filePid, Integer chunkNum, Integer totalChunks, HttpServletRequest request) {

        // 先判断当前用户的容量是否够
        UserFileNumber userFileNumber = fileNumberService.getUserFileNumber(request);
        if (userFileNumber.getCurrentNumber() + fileSize > userFileNumber.getAllNumber()) {
            throw new CenterExceptionHandler("容量不足,请联系管理员扩容");
        }

        Long userId = JwtHelper.getUserId(JwtHelper.getToken(request));

        // 获取临时目录地址
        String tempPath = fileProperties.getTempPath() + "/" + userId;
        // 设置分片名称，按照文件名加用户名加分片索引拼接
        String chunkFileName = fileName + "_chunk_" + chunkNum;

        // 创建临时目录
        File tempDir = new File(tempPath);
        if (!tempDir.exists()) {
            tempDir.mkdirs();
        }

        // 创建分片文件并写入内容
        File chunkFileOnDisk = new File(tempPath + "/" + chunkFileName);
        // 创建父目录
        if (!chunkFileOnDisk.getParentFile().exists()) {
            chunkFileOnDisk.getParentFile().mkdirs();
        }
        try (FileOutputStream outputStream = new FileOutputStream(chunkFileOnDisk, true)) {
            outputStream.write(chunkFile.getBytes());
        } catch (IOException e) {
            throw new CenterExceptionHandler("文件上传失败");
        }
        // 检查所有分片是否已上传
        if (chunkNum == totalChunks - 1) {
            // 创建目标文件
            File targetFile = resolveTargetFile(filePid, fileName, userId);
            // 更新数据库
            saveFileInfo(targetFile, fileSize, filePid, userId);
            // 异步合并
            mergeChunksAsync(targetFile, tempPath, fileName, totalChunks);
            // 返回成功调用
            return FileUploadEnum.SUCCESS;
        } else {
            return FileUploadEnum.UPLOADING;
        }
    }

    /**
     * 获取目标文件路径并解决文件名冲突。
     *
     * @param filePid  父级文件ID
     * @param fileName 文件名
     * @param userId   用户ID
     * @return 确定后的目标文件
     */
    private File resolveTargetFile(String filePid, String fileName, Long userId) {
        String pathStr = "";

        // 递归获取多级父级文件名
        if (!("0".equals(filePid))) {
            fileNameList.clear();
            getParentFileName(filePid);
            Collections.reverse(fileNameList); // 将得到的数组内的元素顺序倒转

            // 合并文件名路径
            pathStr = String.join("/", fileNameList);
        }

        // 获取用户的基础文件路径
        String baseFilePath = fileProperties.getFilePath() + "/" + userId;
        if (!pathStr.isEmpty()) {
            baseFilePath += "/" + pathStr;
        }
        // 获取目标文件路径
        File targetDirectory = FileUtil.mkdir(baseFilePath);
        // 确定目标文件名并解决名称冲突
        return generateUniqueFile(targetDirectory, fileName);
    }

    /**
     * 确定唯一文件名的辅助方法
     *
     * @param directory 目标文件所在目录
     * @param fileName  文件名
     * @return 确定后的文件对象
     */
    private File generateUniqueFile(File directory, String fileName) {
        String baseName = fileName;
        String extension = "";
        int dotIndex = fileName.lastIndexOf('.');

        if (dotIndex != -1) {
            baseName = fileName.substring(0, dotIndex);
            extension = fileName.substring(dotIndex);
        }

        File targetFile = new File(directory, fileName);
        int fileIndex = 1;

        // 如果目标文件已存在，增加序号解决文件名冲突
        while (targetFile.exists()) {
            targetFile = new File(directory, baseName + "(" + fileIndex + ")" + extension);
            fileIndex++;
        }

        return targetFile;
    }

    /**
     * 异步合并文件分片
     *
     * @param tempPath    临时文件路径
     * @param fileName    文件名
     * @param totalChunks 分片总数
     * @return 完成合并操作的CompletableFuture
     */
    @Async
    public CompletableFuture<Void> mergeChunksAsync(File targetFile, String tempPath, String fileName, int totalChunks) {
        // 使用线程池异步执行文件合并操作
        return CompletableFuture.runAsync(() -> mergeChunksIntoSingleFile(targetFile, tempPath, fileName, totalChunks), executorService);
    }

    /**
     * 合并文件
     *
     * @param tempPath    临时路径
     * @param fileName    文件名称
     * @param totalChunks 分块文件
     */
    private void mergeChunksIntoSingleFile(File targetFile, String tempPath, String fileName, Integer totalChunks) {
        try (FileOutputStream outputStream = new FileOutputStream(targetFile)) {
            for (int i = 0; i < totalChunks; i++) {
                String chunkFileName = fileName + "_chunk_" + i;

                File chunkFile = new File(tempPath + "/" + chunkFileName);
                if (!chunkFile.exists()) {
                    throw new CenterExceptionHandler("文件分片丢失");
                }
                try (FileInputStream inputStream = new FileInputStream(chunkFile)) {
                    byte[] buffer = new byte[1024];
                    int bytesRead;
                    while ((bytesRead = inputStream.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, bytesRead);
                    }
                }
            }
        } catch (IOException e) {
            throw new CenterExceptionHandler("文件合并失败");
        } finally {
            for (int i = 0; i < totalChunks; i++) {
                String chunkFileName = fileName + "_chunk_" + i;
                File chunkFile = new File(tempPath + "/" + chunkFileName);
                chunkFile.delete();
            }
        }
    }

    /**
     * 获取文件列表
     *
     * @param fileName   文件名称
     * @param updateTime 修改时间
     * @param request    请求体
     * @return 返回一个树形结构
     */
    @Override
    public Response getFileList(String fileName, Date updateTime, HttpServletRequest request) {
        Long userId = JwtHelper.getLoginId(request);
        LambdaQueryWrapper<FilePan> qw = new LambdaQueryWrapper<>();
        qw.like(fileName != null, FilePan::getFileName, fileName).eq(FilePan::getUserId, userId);

        // 创建日期查询，查询当天内的所有数据
        if (ObjectUtil.isNotEmpty(updateTime)) {
            Date startDate = DateUtil.beginOfDay(updateTime); // 将前端传来的日期转换为当天的开始时间
            Date endDate = DateUtil.endOfDay(startDate); // 将结束日期设置为当天的结束时间
            qw.between(FilePan::getUpdateTime, startDate, endDate);
        }

        // 获取用户文件列表
        List<FilePan> userFileList = this.list(qw);

        // 将文件列表转换为 Map，key 为 fileId，value 为 FilePan 对象
        Map<String, FilePan> filePanMap = new HashMap<>();
        userFileList.forEach(file -> filePanMap.put(file.getFileId(), file));

        // 构建树形结构
        List<FilePan> rootFiles = new ArrayList<>();
        userFileList.forEach(file -> {
            if (file.getFilePid().equals("0")) {
                rootFiles.add(file);
            } else {
                FilePan parent = filePanMap.get(file.getFilePid());
                if (parent != null) {
                    if (parent.getChildren() == null) {
                        parent.setChildren(new ArrayList<>());
                    }
                    parent.getChildren().add(file);
                }
            }
        });

        return Response.rspData(rootFiles);
    }

    /**
     * 获取父级文件名
     *
     * @param filePid
     * @return
     */
    private List<String> getParentFileName(String filePid) {
        LambdaQueryWrapper<FilePan> qw = new LambdaQueryWrapper<>();
        qw.eq(FilePan::getFileId, filePid);
        FilePan one = this.getOne(qw);
        fileNameList.add(one.getFileName());
        if (!"0".equals(one.getFilePid())) {
            getParentFileName(one.getFilePid());
        }
        return fileNameList;
    }

    /**
     * 保存用户上传信息
     *
     * @return
     */
    private Boolean saveFileInfo(File file, Long fileSize, String filePid, Long userId) {
        String filePath = file.getPath();
        String fileName = file.getName();
        String fileType = fileName.substring(fileName.lastIndexOf("."));
        String fileId = RandomUtil.randomString(32);

        FilePan filePan = new FilePan();
        filePan.setFileId(fileId);
        filePan.setFilePid(filePid);
        filePan.setFileName(fileName);
        filePan.setFileSize(fileSize);
        filePan.setFilePath(filePath);
        filePan.setFileType(fileType);
        filePan.setUserId(userId);
        filePan.setFileMd5(MD5.create().digestHex(fileName));
        // 更新用户使用容量
        fileNumberService.updateCurrentNumber(fileSize, userId);
        return save(filePan);
    }

    /**
     * 新建文件夹
     *
     * @param filePan
     * @return
     */
    @Override
    public boolean addDir(FilePan filePan, HttpServletRequest request) {
        LambdaQueryWrapper<FilePan> qw = new LambdaQueryWrapper<>();
        qw.eq(FilePan::getFileName, filePan.getFileName());
        qw.eq(FilePan::getFilePid, filePan.getFilePid());
        qw.eq(FilePan::getUserId, JwtHelper.getLoginId(request));
        FilePan one = this.getOne(qw);
        if (one != null) {
            throw new CenterExceptionHandler("该文件夹已存在");
        }

        filePan.setFileId(RandomUtil.randomString(32));
        filePan.setUserId(JwtHelper.getLoginId(request));
        filePan.setFileMd5(MD5.create().digestHex(filePan.getFileName()));

        String pathStr = "";
        // 递归获取多级父级文件名
        if (!("0".equals(filePan.getFilePid()))) {
            fileNameList.clear();
            fileNameList.add(filePan.getFileName());
            getParentFileName(filePan.getFilePid());
            Collections.reverse(fileNameList); // 将得到的数组内的元素顺序倒转
            pathStr = fileNameList.stream().reduce((a, b) -> a + "/" + b).get();
        }
        // 获取目标文件路径
        String targetFilePath = fileProperties.getFilePath() + "/" + JwtHelper.getLoginId(request);
        if (StrUtil.isNotEmpty(pathStr)) {
            targetFilePath = targetFilePath + "/" + pathStr;
        } else {
            targetFilePath = targetFilePath + "/" + filePan.getFileName();
        }
        filePan.setFilePath(targetFilePath);
        FileUtil.mkdir(targetFilePath);
        return save(filePan);
    }

    /**
     * 根据文件路径删除真实文件和数据库数据
     */
    @Override
    @Transactional
    public void deleteFile(FilePan filePan, HttpServletRequest request) {
        // 判断是否还有子文件
        LambdaQueryWrapper<FilePan> qw = new LambdaQueryWrapper<>();
        qw.eq(FilePan::getUserId, JwtHelper.getLoginId(request));
        qw.eq(FilePan::getFilePid, filePan.getFileId());
        if (count(qw) > 0) {
            throw new CenterExceptionHandler("该文件夹下还有子文件，请先删除子文件");
        }

        // 获取当前要删除文件的大小
        FilePan nowFileInfo = this.getOne(new LambdaQueryWrapper<FilePan>().eq(FilePan::getFileId, filePan.getFileId()));
        Long fileSize = nowFileInfo.getFileSize();
        // 更新用户使用容量
        fileNumberService.updateCurrentNumber(-fileSize, JwtHelper.getLoginId(request));

        // 删除数据库文件
        LambdaUpdateWrapper<FilePan> uw = new LambdaUpdateWrapper<>();
        uw.eq(FilePan::getUserId, JwtHelper.getLoginId(request));
        uw.eq(FilePan::getFileId, filePan.getFileId());
        this.remove(uw);

        // 删除磁盘文件
        FileUtil.del(filePan.getFilePath());

        // 删除同名的切片文件
        String md5Dir = FileChunkUtil.createMd5Dir(filePan.getFilePath());
        if (FileUtil.exist(md5Dir)) {
            FileUtil.del(md5Dir);
        }
    }

    /**
     * 文件预览
     */
    @Override
    public void preview(FilePan filePan, HttpServletResponse response) {
        // 区分图片还是视频
        if (FileTypeUtil.isImage(filePan.getFileName())) {
            previewImg(filePan, response);
        } else if (FileTypeUtil.isVideo(filePan.getFileName())) {
            previewVideo(filePan, response);
        } else {
            throw new CenterExceptionHandler("该文件不支持预览");
        }
    }

    /**
     * 图片预览
     *
     * @param filePan
     * @param response
     */
    private void previewImg(FilePan filePan, HttpServletResponse response) {
        if (StrUtil.isEmpty(filePan.getFileId())) {
            return;
        }
        // 源文件路径
        String realTargetFile = filePan.getFilePath();
        File file = new File(filePan.getFilePath());
        if (!file.exists()) {
            return;
        }
        readFile(response, realTargetFile);
    }

    /**
     * 视频预览
     *
     * @param filePan
     * @param response
     */
    private void previewVideo(FilePan filePan, HttpServletResponse response) {
        // 根据文件名称创建对应的MD5文件夹
        String md5Dir = FileChunkUtil.createMd5Dir(filePan.getFilePath());
        // 去这个目录下查看是否有index.m3u8这个文件
        String m3u8Path = md5Dir + "/" + FileConstants.M3U8_NAME;
        if (!FileUtil.exist(m3u8Path)) {
            // 创建视频ts文件和索引文件
            createVideoTs(filePan.getFilePath(), filePan.getFileId(), md5Dir, response);
        } else {
            // 读取索引文件
            readFile(response, m3u8Path);
        }
    }

    // 创建视频切片文件
    private void createVideoTs(String videoPath, String fileId, String targetPath, HttpServletResponse response) {
        // 1.生成ts文件
        String video_2_TS = "ffmpeg -y -i %s -vcodec copy -acodec copy -bsf:v h264_mp4toannexb %s";
        String tsPath = targetPath + "/" + FileConstants.TS_NAME;
        String cmd = String.format(video_2_TS, videoPath, tsPath);
        ProcessUtils.executeCommand(cmd, false);

        // 2.创建切片文件
        String ts_chunk = "ffmpeg -i %s -c copy -map 0 -f segment -segment_list %s -segment_time 60 %s/%s_%%4d.ts";
        String m3u8Path = targetPath + "/" + FileConstants.M3U8_NAME;
        cmd = String.format(ts_chunk, tsPath, m3u8Path, targetPath, fileId);
        ProcessUtils.executeCommand(cmd, false);

        // 删除index.ts文件
        FileUtil.del(tsPath);

        // 读取切片文件
        readFile(response, m3u8Path);
    }

    /**
     * 加载视频切片文件
     *
     * @param tsFileId 切片文件ID
     * @param response 响应
     */
    @Override
    public void readFileTs(String tsFileId, HttpServletResponse response) {
        String[] tsArray = tsFileId.split("_");
        String videoFileId = tsArray[0];
        LambdaUpdateWrapper<FilePan> qw = new LambdaUpdateWrapper<>();
        qw.eq(FilePan::getFileId, videoFileId);
        FilePan one = this.getOne(qw);
        // 获取文件对应的MD5文件夹地址
        String md5Dir = FileChunkUtil.createMd5Dir(one.getFilePath());
        // 去MD5目录下读取ts分片文件
        String tsFile = md5Dir + "/" + tsFileId;
        readFile(response, tsFile);
    }

    /**
     * 重命名文件
     *
     * @param filePan 文件信息
     * @param request 请求体
     * @return 更新结果
     */
    @Override
    public boolean renameFileName(FilePan filePan, HttpServletRequest request) {
        File file = new File(filePan.getFilePath());

        // 更新磁盘文件名
        FileUtil.rename(file, filePan.getFileName(), true);

        return update(new LambdaUpdateWrapper<FilePan>()
                .eq(FilePan::getFileId, filePan.getFileId())
                .eq(FilePan::getUserId, JwtHelper.getLoginId(request))
                .set(FilePan::getFileName, filePan.getFileName())
                .set(FilePan::getFilePath, file.getParent() + "/" + filePan.getFileName())
                .set(FilePan::getFileMd5, MD5.create().digestHex(filePan.getFileName()))
        );
    }

    /**
     * 文件下载方法
     *
     * @param fileId   文件id
     * @param response 响应体
     */
    @Override
    public void download(String fileId, HttpServletResponse response) {
        if (StrUtil.isEmpty(fileId)) {
            throw new CenterExceptionHandler("文件ID不能为空");
        }
        FilePan one = this.getOne(new LambdaQueryWrapper<FilePan>().eq(FilePan::getFileId, fileId));
        readFile(response, one.getFilePath());
    }

    /**
     * 批量删除
     *
     * @param fileIds
     * @param request
     * @return
     */
    @Override
    public boolean batchDelete(String fileIds, HttpServletRequest request) {
        LambdaQueryWrapper<FilePan> qw = new LambdaQueryWrapper<>();
        qw.eq(FilePan::getUserId, JwtHelper.getLoginId(request));
        qw.in(FilePan::getFileId, fileIds.split(","));

        List<FilePan> list = list(qw);

        list.forEach(filePan -> {
            deleteFile(filePan, request);
        });

        return true;
    }
}
